package com.gitee.wsl.jvm.reflect


import com.gitee.wsl.jvm.exception.FieldNotFoundException
import com.gitee.wsl.jvm.exception.JobNotFoundException
import com.gitee.wsl.jvm.exception.JobRunrException.Companion.shouldNotHappenException
import com.gitee.wsl.jvm.reflect.autobox.Autoboxer

import java.lang.reflect.AccessibleObject
import java.lang.reflect.Constructor
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.nio.file.Path
import java.security.AccessController
import java.security.PrivilegedAction
import java.util.Arrays
import java.util.Locale
import java.util.Optional
import java.util.function.Predicate
import java.util.function.Supplier
import java.util.stream.Stream

object ReflectionUtils {
    private const val ROOT_PACKAGE_NAME = "org/jobrunr/"
    private val PRIMITIVE_TO_TYPE_MAPPING: MutableMap<Class<*>?, Class<*>> = HashMap()

    init {
        PRIMITIVE_TO_TYPE_MAPPING[Boolean::class.javaPrimitiveType] = Boolean::class.java
        PRIMITIVE_TO_TYPE_MAPPING[Byte::class.javaPrimitiveType] = Byte::class.java
        PRIMITIVE_TO_TYPE_MAPPING[Short::class.javaPrimitiveType] = Short::class.java
        PRIMITIVE_TO_TYPE_MAPPING[Char::class.javaPrimitiveType] =
            Char::class.java
        PRIMITIVE_TO_TYPE_MAPPING[Int::class.javaPrimitiveType] = Int::class.java
        PRIMITIVE_TO_TYPE_MAPPING[Long::class.javaPrimitiveType] =
            Long::class.java
        PRIMITIVE_TO_TYPE_MAPPING[Float::class.javaPrimitiveType] = Float::class.java
        PRIMITIVE_TO_TYPE_MAPPING[Double::class.javaPrimitiveType] = Double::class.java
    }

    fun classExists(className: String?): Boolean {
        try {
            loadClass(className)
            return true
        } catch (e: ClassNotFoundException) {
            return false
        }
    }

    fun <T> toClassFromPath(path: Path): Class<T>? {
        val classFile = path.toString().substring(path.toString().indexOf(ROOT_PACKAGE_NAME))
        val className = toClassNameFromFileName(classFile)
        return toClass(className)
    }

    fun toClassNameFromFileName(classFile: String): String {
        return classFile.replace(".class", "").replace("/", ".")
    }

    fun <T> toClass(className: String): Class<T> {
        return when (className) {
            "boolean" -> cast(Boolean::class.javaPrimitiveType!!)
            "byte" -> cast(Byte::class.javaPrimitiveType!!)
            "short" -> cast(Short::class.javaPrimitiveType!!)
            "int" -> cast(Int::class.javaPrimitiveType!!)
            "long" -> cast(Long::class.javaPrimitiveType!!)
            "float" -> cast(Float::class.javaPrimitiveType!!)
            "double" -> cast(Double::class.javaPrimitiveType!!)
            "char" -> cast(Char::class.javaPrimitiveType!!)
            "void" -> cast<T>(Void.TYPE)
            else -> try {
                cast<T>(loadClass(className))
            } catch (ex: ClassNotFoundException) {
                throw IllegalArgumentException("Class not found: $className", ex)
            }
        }
    }

    @Throws(ClassNotFoundException::class)
    fun loadClass(className: String?): Class<*> {
        // why: support for quarkus:dev (see https://github.com/quarkusio/quarkus/issues/2809) and Spring Boot Live reload
        // Jackson uses this order also
        try {
            val classLoader = Thread.currentThread().contextClassLoader
            if (classLoader != null) {
                return Class.forName(className, true, classLoader)
            }
        } catch (e: ClassNotFoundException) {
            // support for Spring Boot Executable jar. See https://github.com/jobrunr/jobrunr/issues/81
        }
        return Class.forName(className)
    }

    fun hasDefaultNoArgConstructor(clazzName: String): Boolean {
        return Stream.of(
            *toClass<Any>(clazzName).constructors
        )
            .anyMatch { c: Constructor<*> -> c.parameterCount == 0 }
    }

    fun hasDefaultNoArgConstructor(clazz: Class<*>): Boolean {
        return Stream.of(*clazz.constructors)
            .anyMatch { c: Constructor<*> -> c.parameterCount == 0 }
    }

    fun <T> newInstanceAndSetFieldValues(clazz: Class<T>, fieldValues: Map<String?, String?>): T {
        val t = newInstance(clazz)
        val declaredFields = clazz.declaredFields
        for (field in declaredFields) {
            setFieldUsingAutoboxing(field, t, fieldValues[field.name])
        }
        return t
    }

    fun <T> newInstance(className: String, vararg params: Any?): T {
        return newInstance(toClass(className), *params)
    }

    fun <T> newInstance(clazz: Class<T>, vararg params: Any?): T {
        try {
            return newInstanceCE(clazz, *params)
        } catch (e: ReflectiveOperationException) {
            throw shouldNotHappenException(e)
        }
    }

    @Throws(ReflectiveOperationException::class)
    fun <T> newInstanceCE(clazz: Class<T>, vararg params: Any?): T {
        val declaredConstructor = getConstructorForArgs<T>(
            clazz,
            Stream.of<Any>(*params).map { obj: Any -> obj.javaClass }.toArray() as Array<Class<*>>
        )
                //.toArray<Class<*>> { _Dummy_.__Array__() })
        makeAccessible(declaredConstructor)
        return declaredConstructor.newInstance(*params)
    }

    @Throws(ReflectiveOperationException::class)
    fun <T> newInstanceCE(clazz: Class<T>): T {
        val defaultConstructor = clazz.getDeclaredConstructor()
        makeAccessible(defaultConstructor)
        return defaultConstructor.newInstance()
    }

    fun <T> newInstance(clazz: Class<T>): T {
        try {
            val defaultConstructor = clazz.getDeclaredConstructor()
            makeAccessible(defaultConstructor)
            return defaultConstructor.newInstance()
        } catch (e: ReflectiveOperationException) {
            throw shouldNotHappenException(e)
        }
    }

    fun getMethod(clazz: Class<*>, methodName: String, vararg parameterTypes: Class<*>): Method {
        return findMethod(clazz, methodName, *parameterTypes)
            .orElseThrow<RuntimeException>(Supplier<RuntimeException> {
                JobNotFoundException(
                    clazz,
                    methodName,
                    parameterTypes as Array<Class<*>>
                )
            })
    }

    fun findMethod(
        `object`: Any,
        methodName: String,
        vararg parameterTypes: Class<*>
    ): Optional<Method> {
        return findMethod(`object`.javaClass, MethodFinderPredicate(methodName, *parameterTypes))
    }

    fun findMethod(
        clazz: Class<*>,
        methodName: String,
        vararg parameterTypes: Class<*>
    ): Optional<Method> {
        return findMethod(clazz, MethodFinderPredicate(methodName, *parameterTypes))
    }

    fun findMethod(clazz: Class<*>, predicate: Predicate<Method>): Optional<Method> {
        val optionalMethod = Arrays.stream(clazz.declaredMethods)
            .filter(predicate)
            .findFirst()
        return if (optionalMethod.isPresent) {
            optionalMethod
        } else if (clazz.isInterface) {
            Stream.of(*clazz.interfaces)
                .map { superInterface: Class<*> -> findMethod(superInterface, predicate) }
                .filter { obj: Optional<Method> -> obj.isPresent }
                .findFirst()
                .orElse(Optional.empty())
        } else if (Any::class.java != clazz.superclass) {
            findMethod(clazz.superclass, predicate)
        } else {
            Optional.empty()
        }
    }

    fun getField(clazz: Class<*>, fieldName: String): Field {
        return findField(clazz, fieldName)
            .orElseThrow<RuntimeException>(Supplier<RuntimeException> {
                FieldNotFoundException(
                    clazz,
                    fieldName
                )
            })
    }

    fun findField(clazz: Class<*>, fieldName: String): Optional<Field> {
        return findField(clazz) { f: Field -> fieldName == f.name }
    }

    fun findField(clazz: Class<*>, predicate: Predicate<Field>?): Optional<Field> {
        val optionalField = Arrays.stream(clazz.declaredFields)
            .filter(predicate)
            .findFirst()
        return if (optionalField.isPresent) {
            optionalField
        } else if (Any::class.java != clazz.superclass) {
            findField(clazz.superclass, predicate)
        } else {
            Optional.empty()
        }
    }

    fun isClassAssignableToObject(clazz: Class<*>, `object`: Any): Boolean {
        return isClassAssignable(clazz, `object`.javaClass)
    }

    @JvmStatic
    fun isClassAssignable(clazz1: Class<*>, clazz2: Class<*>): Boolean {
        return (clazz1 == clazz2 || clazz1.isAssignableFrom(clazz2)
                || (clazz1.isPrimitive && PRIMITIVE_TO_TYPE_MAPPING[clazz1] == clazz2)
                || (clazz1.isPrimitive && java.lang.Boolean.TYPE == clazz1 && Int::class.java == clazz2))
    }

    fun objectContainsFieldOrProperty(`object`: Any?, fieldName: String): Boolean {
        if (`object` == null) return false
        return objectContainsField(`object`, fieldName) || objectContainsProperty(
            `object`,
            fieldName
        )
    }

    fun getValueFromFieldOrProperty(`object`: Any, paramName: String): Any {
        val aClass: Class<*> = `object`.javaClass
        val optionalField = findField(aClass, paramName)
        if (optionalField.isPresent) {
            return getValueFromField(optionalField.get(), `object`)
        }

        val optionalGetMethod = findMethod(aClass, "get" + paramName.capitalize())
        if (optionalGetMethod.isPresent) {
            return getValueFromGetMethod(optionalGetMethod.get(), `object`)
        }

        throw IllegalArgumentException(
            String.format(
                "Could not get value '%s' from object with class %s",
                paramName,
                `object`.javaClass
            )
        )
    }

    fun getValueFromField(field: Field, `object`: Any): Any {
        try {
            makeAccessible(field)
            return field[`object`]
        } catch (willNotHappen: ReflectiveOperationException) {
            throw IllegalArgumentException(
                String.format(
                    "Could not get value '%s' from object with class %s",
                    field.name,
                    `object`.javaClass
                )
            )
        }
    }

    fun getValueFromGetMethod(getter: Method, `object`: Any): Any {
        try {
            makeAccessible(getter)
            return getter.invoke(`object`)
        } catch (willNotHappen: ReflectiveOperationException) {
            throw IllegalArgumentException(
                String.format(
                    "Could not get value '%s' from object with class %s",
                    getter.name,
                    `object`.javaClass
                )
            )
        }
    }

    fun setFieldUsingAutoboxing(fieldName: String, `object`: Any, value: Any?) {
        if (value == null) return

        setFieldUsingAutoboxing(getField(`object`.javaClass, fieldName), `object`, value)
    }

    fun setFieldUsingAutoboxing(field: Field, `object`: Any?, value: Any?) {
        try {
            if (value == null) return

            makeAccessible(field)
            val type = field.type

            val fieldValue = autobox(value, type)
            field[`object`] = fieldValue
        } catch (e: ReflectiveOperationException) {
            throw shouldNotHappenException(e)
        }
    }

    fun <T> autobox(value: Any, type: Class<T>): T? {
        return Autoboxer.autobox(value, type)
    }

    fun makeAccessible(accessibleObject: AccessibleObject) {
        AccessController.doPrivileged(PrivilegedAction<Void?> {
            accessibleObject.isAccessible = true
            null
        })
    }

    @Throws(NoSuchMethodException::class)
    private fun <T> getConstructorForArgs(clazz: Class<T>?, args: Array<Class<*>>): Constructor<T> {
        val constructors = clazz!!.constructors

        for (constructor in constructors) {
            val types = constructor.parameterTypes
            if (types.size == args.size) {
                var argumentsMatch = true
                for (i in args.indices) {
                    if (!types[i].isAssignableFrom(args[i])) {
                        argumentsMatch = false
                        break
                    }
                }

                if (argumentsMatch) return cast(constructor)
            }
        }

        return clazz.getConstructor(*args)
    }

    /**
     * Why: less warnings and @SuppressWarnings("unchecked")
     */
    private fun <T> cast(aClass: Class<*>): Class<T> {
        return aClass as Class<T>
    }

    /**
     * Why: less warnings and @SuppressWarnings("unchecked")
     */
    fun <T> cast(anObject: Any): T {
        return anObject as T
    }

    private fun objectContainsField(`object`: Any, fieldName: String): Boolean {
        return findField(`object`.javaClass, fieldName).isPresent
    }

    private fun objectContainsProperty(`object`: Any, fieldName: String): Boolean {
        return findMethod(`object`.javaClass, "get" + fieldName.replaceFirstChar {
            if (it.isLowerCase()) it.titlecase(
                Locale.ROOT
            ) else it.toString()
        }).isPresent
    }
}
